package me.smileface.nio.chat;

import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/*
    网络聊天室
    1、编写Java NIO服务端程序，支持多个客户端同时连接
    2、客户端初次连接时，服务端提示输入昵称，如果昵称有人占用，提示重新输入，如果昵称唯一，登陆成功
       按照一定的规则的组织数据（协议）（比如：昵称#@#你好！！)
    3、有新的客户端登录以后，群发欢迎信息同时统计在线人数，并且这些信息要通知到所有的在线客户
    4、服务器收到已登录客户端发送的内容，转发给其他所有在线的客户端
*/
public class NIOChatServer {

    private int port = 8080;
    private Charset charset = StandardCharsets.UTF_8;
    // 在线人数
    private HashSet<String> users = new HashSet<>();

    private Selector selector = null;
    private ServerSocketChannel ssc = null;

    public static void main(String[] args) {
        try {
            new NIOChatServer(8080).listen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public NIOChatServer(int port) throws IOException {
        this.port = port;

        initServer();
    }

    private void initServer() throws IOException {
        ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(this.port));
        ssc.configureBlocking(false);

        selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("服务端已启动，监听端口：" + this.port);
    }

    private void listen() throws IOException {
        while (true) {
            // selector.select() 不会阻塞
            int wait = selector.select();
            if (wait == 0) continue;

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                process(key);
            }
        }
    }

    private void process(SelectionKey key) throws IOException {
        if (!key.isValid()) return;

        if (key.isAcceptable()) {
            ServerSocketChannel socketChannel = (ServerSocketChannel) key.channel();
            SocketChannel clientChannel = socketChannel.accept();

            System.out.println("客户端已连接：" + clientChannel);

            clientChannel.configureBlocking(false);
            SelectionKey newKey = clientChannel.register(selector, SelectionKey.OP_READ);

            // 继续接收新的连接请求
            key.interestOps(SelectionKey.OP_ACCEPT);

            clientChannel.write(charset.encode("请输入你的昵称:"));
        }

        // 接收到客户端的数据
        if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            StringBuilder sb = new StringBuilder();
            while (client.read(buffer) > 0) {
                buffer.flip();
                sb.append(charset.decode(buffer));
            }

            // 继续接收客户端的写入
            key.interestOps(SelectionKey.OP_READ);

            // 接收消息处理
            processMsg(sb.toString(), client);
        }
    }

    private void processMsg(String msg, SocketChannel client) throws IOException {
        if (StringUtils.isEmpty(msg)) return;

        String[] parts = msg.split(ChatConst.USER_CONTENT_SPLIT);
        if (parts.length == 1) {
            String nickName = parts[0];
            if (users.contains(nickName)) {
                client.write(charset.encode(ChatConst.USER_EXISTS));
            } else {
                users.add(nickName);
                int onlineCnt = getOnlineCount();
                String reply = "欢迎 " + nickName + " 加入聊天室，当前在线人数：" + onlineCnt;
                broadcast(null, reply);
            }
        } else if (parts.length > 1) {
            String nickName = parts[0];
            String content = parts[1];

            if (users.contains(nickName)) {
                broadcast(client, nickName + " 说 " + content);
            }

        }
    }

    private void broadcast(SocketChannel excludeClient, String content) throws IOException {
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key : keys) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel && channel != excludeClient) {
                SocketChannel target = (SocketChannel) channel;
                target.write(charset.encode(content));
            }
        }
    }

    private int getOnlineCount() {
        int count = 0;
        for (SelectionKey key : selector.keys()) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel) {
                count += 1;
            }
        }
        return count;
    }

}
